#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Class decorator with Private and Public attribute declarations.
Controls access to attributes stored on an instance, or inherited
by it from its classes. Private declares attribute names that
cannot be fetched or assigned outside the decorated class, and
Public declares all the names that can. Caveat: this works in
3.0 for normally named attributes only: __X__ operator overloading
methods implicitly run for built-in operations do not trigger
either __getattr__ or __getattribute__ in new-style classes.
Add __X__ methods here to intercept and delegate built-ins.
"""


def accessControl(failIf, trace=True):
    def onDecorator(cls):
        class onInstance:
            def __init__(self, *args, **kwargs):
                self.__wrapped = cls(*args, **kwargs)

            def __getattr__(self, attr):
                if trace:
                    print('[{0}]'.format(' '.join(map(str, ['get:', attr]))))
                    if failIf(attr):
                        raise TypeError('private attribute fetch: {0}'.format(attr))
                return getattr(self.__wrapped, attr)

            def __setattr__(self, attr, value):
                if trace:
                    print('[{0}]'.format(' '.join(map(str, ['set:', attr, value]))))
                    if attr == '_onInstance__wrapped':
                        self.__dict__[attr] = value
                    elif failIf(attr):
                        raise TypeError('private attribute change: {0}'.format(attr))
                self.__dict__[attr] = value

        return onInstance
    return onDecorator


def Private(*attributes, trace=True):
    return accessControl(failIf=lambda attr: attr in attributes, trace=trace)


def Public(*attributes, trace=True):
    return accessControl(failIf=lambda attr: attr not in attributes, trace=trace)


if __name__ == '__main__':
    @Private('data', 'size', trace=True)
    class Doubler:
        def __init__(self, label, start):
            self.label = label
            self.data = start

        def size(self):
            return len(self.data)

        def double(self):
            for i in range(self.size()):
                self.data[i] = self.data[i] * 2

        def display(self):
            print('{0} => {1}'.format(self.label, self.data))

    X = Doubler('X is', [1, 2, 3])
    Y = Doubler('Y is', [-10, -20, -30])
    print('')
    # succeed
    print(X.label)
    X.display(); X.double(); X.display()
    print(Y.label)
    Y.display(); Y.double(); Y.display()
    Y.label = 'Spam'
    Y.display()
    print('')
    # fail
    try:
        print(X.size())
    except TypeError as e:
        print(e, end='\n')
    try:
        print(X.data)
    except TypeError as e:
        print(e, end='\n')
    try:
        print(X.data)
    except TypeError as e:
        print(e, end='\n')
    try:
        X.data = [1, 1, 1]
    except TypeError as e:
        print(e, end='\n')
    try:
        X.size = lambda S: 0
    except TypeError as e:
        print(e, end='\n')
    try:
        print(Y.data)
    except TypeError as e:
        print(e, end='\n')
    try:
        print(Y.size())
    except TypeError as e:
        print(e, end='\n')

